#include "BankClient.h"
#include "macros.h"
#include "CommandLineHelper.h"
#include <stdexcept>
#include <iostream>
#include <fstream>
#include <sstream>
#include <gtest/gtest.h>
#include <stdlib.h>

using namespace std;

#define UPDATE_COUNT_FILE "count.dat"

////////////////////////////////
//                            //
// Local function definitions //
//                            //
////////////////////////////////

template <typename T> bool
ValueIsValid (T ret)
  {
  return true;
  }

template <> bool
ValueIsValid (double ret)
  {
  return ret >= 0;
  }

template <> bool
ValueIsValid (int ret)
  {
  return ret >= 0;
  }

/// \return true if the reading is done successfully; false otherwise
template <typename T> bool 
ReadCinTo (T& ret)
  {
  bool error;
  
  do
    {
    string str;

    cout << "> ";
    getline (cin, str);

    istringstream iss (str);
    iss >> ret; 

    error = !iss || !iss.eof () || !ValueIsValid (ret);
    if (error)
      {
      cout << "ERR - Invalid input!" << endl;
      }
    } while (error);
  }

/**
 * Get the expected UpdateCount from the file
 *
 * \param filename the file from which the expected UpdateCount is read
 * \return the lastest UpdateCount expected by the client
 */
int
GetExpectedUpdateCount (string filename)
  {
  // do "touch <file>"
  ofstream touch (filename.c_str (), ios_base::app | ios_base::out);
  CHECK_STREAM_ERROR (touch, "Unable to open file - " << filename);
  touch.close ();

  ifstream read (filename.c_str ());
  CHECK_STREAM_ERROR (read, "Unable to open file - " << filename);

  int count;
  string count_str;
  getline (read, count_str);
  //DEBUG_LOG ("after \"getline (read, count_str);\", count_str = " << count_str);
  //DEBUG_LOG ("count_str.empty () = " << count_str.empty ());
  if (count_str.empty ())
    {
    count = 0;
    }
  else
    {
    istringstream count_ss (count_str);
    count_ss >> count;
    CHECK_STREAM_ERROR (count_ss, "Failed reading stream 'count_ss' into (int) count");
    }
  read.close ();

  return count;
  }

/**
 * Increment the expected UpdateCount that stored in a file
 *
 * \param filename the file in which the expected UpdateCount is incremented by 1
 */
void 
IncrementExpectedUpdateCount (string filename)
  {
  int count = GetExpectedUpdateCount (filename);

  //DEBUG_LOG ("after \"int count = GetExpectedUpdateCount (filename);\", count = " << count);

  ofstream dat (filename.c_str ());
  CHECK_STREAM_ERROR (dat, "Unable to open file - " << filename);

  dat << count+1 << endl;
  CHECK_STREAM_ERROR (dat, "Failed writing to ofstream 'dat' from count+1 (" << count+1 << ")");

  dat.close ();
  }
 
//////////////////////
//                  //
// Class definition //
//                  //
//////////////////////

BankClient::BankClient (string primarySvc, string backupSvc) :
  m_cusId (), m_clnt (0), m_primarySvc (primarySvc), m_backupSvc (backupSvc), m_primaryIsActive (true)
  {
  // creating client for the pimary server
	CreateClient (primarySvc);
  m_primaryIsActive = true;

  if (m_clnt == NULL)
    {
    DEBUG_WRN ("Primary server (" << m_primarySvc << ") is unreachable.");
    CreateClient (backupSvc);
    m_primaryIsActive = false;

    if (m_clnt == NULL)
      {
      string err;
      WRITE_STRING (err, "Both primary server (" 
        << m_primarySvc << ") and backup server (" 
        << m_backupSvc << ") are all unreachable!");
      throw runtime_error (err);
      }
    }
  }

BankClient::~BankClient ()
  {
  if (m_clnt != NULL)
    {
    clnt_destroy (m_clnt);
    }
  }

/// \return false if RPC call failed or server logic failed; true otherwise
template <typename RET, typename ARGS>
bool 
BankClient::RpcCalling (ARGS args, RET* (*RpcFunc)(ARGS*,CLIENT*))
  {
  const int maxRetry = 2;
  for (int retry=0; retry<maxRetry; retry++)
    {
	  RET* result = RpcFunc (&args, m_clnt);
	  if (result == (ActionRet_t *) NULL) 
      {
		  DEBUG_WRN (clnt_sperror (m_clnt, "call failed"));

      // by using the short-circuit behavior, it tries to switch two times if switching once is still failed
      if (SwitchServer ())
        {
        continue;
        }
      else
        {
        throw runtime_error ("RpcCalling () is unable to switch to the inactive server!");
        }
	    }
    if (!result->status)
      {
      cout << "OK - " << result->statusMsg.statusMsg_val << endl;
      }
    else
      {
      cout << "ERR - " << result->statusMsg.statusMsg_val << endl;
      };

    return !result->status;
    }

  string err;
  WRITE_STRING (err, "RpcCalling () retried " 
    << maxRetry << " times but still failed to call the active server!");
  throw runtime_error (err);
  }

void
BankClient::AskLogin ()
  {
  cout << "Enter your customer ID: " << endl;
  ReadCinTo (m_cusId);
  }

BankClient::CustomerAction_t
BankClient::AskAction ()
  {
  int action;

  cout << endl;
  cout << "Select an instruction below by typing the number:" << endl;
  cout << "  " << Withdrawal  << ". Withdrawal" << endl;
  cout << "  " << Deposit     << ". Deposit" << endl;
  cout << "  " << Transfer    << ". Transfer" << endl;
  cout << "  " << ReadBalance << ". Read Balance" << endl;
  cout << "  " << Exit        << ". Exit" << endl;
  cout << m_cusId;
  ReadCinTo (action);

  return (CustomerAction_t) action;
  }

void 
BankClient::DoAction (CustomerAction_t action)
  {
  switch (action)
    {
    case Withdrawal:
      DoWithdrawal (); 
      break;
    case Deposit:
      DoDeposit ();
      break;
    case Transfer:
      DoTransfer ();
      break;
    case ReadBalance:
      DoReadBalance ();
      break;
    case Exit:
      break;
    default:
      cout << "Unknown instruction!" << endl;
    };
  }

bool 
BankClient::CreateClient (string svc)
  {
	m_clnt = clnt_create (svc.c_str (), SyncAgent, SyncAgentVer, "udp");
	if (m_clnt == NULL) 
    {   
	  DEBUG_WRN ("BankClient is unable to create a client handle for a primary server - " 
      << clnt_spcreateerror (svc.c_str ()));
    return false;
    }
  else
    {
    return true;
    }
  }

bool 
BankClient::SwitchServer ()
  {
  bool ret;

  if (m_clnt != NULL)
    {
    clnt_destroy (m_clnt);
    }

  // try switching
  if (m_primaryIsActive)
    {
    CreateClient (m_backupSvc);
    DEBUG_LOG ("Switching to backup server (" << m_backupSvc << ")");
    }
  else
    {
    CreateClient (m_primarySvc);
    DEBUG_LOG ("Switching to primary server (" << m_primarySvc << ")");
    }

  if (m_clnt == NULL)
    { // failed creating client to the inactive server. undo
    if (m_primaryIsActive)
      {
      CreateClient (m_primarySvc);
      }
    else
      {
      CreateClient (m_backupSvc);
      }

    if (m_clnt == NULL)
      {  // undo failed!!!
      throw runtime_error ("SwitchServer () aborted as both primary and backup servers are unreachable!");
      }
    else
      {
      DEBUG_WRN ("No server switching can be done as the other server is unreachable");
      ret = true;
      }
    }
  else
    { // switching OK
    m_primaryIsActive = !m_primaryIsActive;
    DEBUG_LOG ("Switching to the inactive server successfully");
    ret = true;
    }

  return ret;
  }

bool
BankClient::DoWithdrawal ()
  {
  double amount;

  cout << endl;
  cout << "Amount of money to withdrawal:" << endl;
  ReadCinTo (amount);

  ClnWdDpArgs_t args;
  args.amount = amount;
  args.cusId.cusId_val = const_cast<char*> (m_cusId.c_str ());
  args.cusId.cusId_len = m_cusId.size ();
  args.expectedUpdateCount = GetExpectedUpdateCount (UPDATE_COUNT_FILE) + 1;

  if (RpcCalling (args, &clnwithdrawal_1))
    {
    IncrementExpectedUpdateCount (UPDATE_COUNT_FILE);
    return true;
    }
  else
    {
    return false;
    }
  }

bool
BankClient::DoDeposit ()
  {
  double amount;

  cout << endl;
  cout << "Amount of money to deposit:" << endl;
  ReadCinTo (amount);

  ClnWdDpArgs_t args;
  args.amount = amount;
  args.cusId.cusId_val = const_cast<char*> (m_cusId.c_str ());
  args.cusId.cusId_len = m_cusId.size ();
  args.expectedUpdateCount = GetExpectedUpdateCount (UPDATE_COUNT_FILE) + 1;

  if (RpcCalling (args, &clndeposit_1))
    {
    IncrementExpectedUpdateCount (UPDATE_COUNT_FILE);
    return true;
    }
  else
    {
    return false;
    }
  }

bool
BankClient::DoTransfer ()
  {
  cout << endl;

  double amount;
  cout << "Amount of money to transfer:" << endl;
  ReadCinTo (amount);

  string toCusId;
  cout << "Customer ID to whom the money goes:" << endl;
  ReadCinTo (toCusId);

  ClnTransferArgs_t args;
  args.amount = amount;
  args.fromCusId.fromCusId_val = const_cast<char*> (m_cusId.c_str ());
  args.fromCusId.fromCusId_len = m_cusId.size ();
  args.toCusId.toCusId_val = const_cast<char*> (toCusId.c_str ());
  args.toCusId.toCusId_len = toCusId.size ();

  args.expectedUpdateCount = GetExpectedUpdateCount (UPDATE_COUNT_FILE) + 1;

  if (RpcCalling (args, &clntransfer_1))
    {
    IncrementExpectedUpdateCount (UPDATE_COUNT_FILE);
    return true;
    }
  else
    {
    return false;
    }
  }

bool
BankClient::DoReadBalance ()
  {
  ClnReadBalanceArgs_t args;
  args.cusId.cusId_val = const_cast<char*> (m_cusId.c_str ());
  args.cusId.cusId_len = m_cusId.size ();

  args.expectedUpdateCount = GetExpectedUpdateCount (UPDATE_COUNT_FILE) + 1;

  return RpcCalling (args, &clnreadbalance_1);  
  }

bool
BankClient::DoCreateCustomer ()
  {
  ClnReadBalanceArgs_t args;
  args.cusId.cusId_val = const_cast<char*> (m_cusId.c_str ());
  args.cusId.cusId_len = m_cusId.size ();

  args.expectedUpdateCount = GetExpectedUpdateCount (UPDATE_COUNT_FILE) + 1;

  if (RpcCalling (args, &clncreateclient_1))
    {
    IncrementExpectedUpdateCount (UPDATE_COUNT_FILE);
    return true;
    }
  else
    {
    return false;
    }
  }

void 
BankClient::Run ()
  {
  AskLogin ();
  DoCreateCustomer ();

  CustomerAction_t action;
  do
    {
    action = AskAction ();
    DoAction (action);
    } while (action != Exit);
  }

///////////////////////////
//                       //
// Test cases definition //
//                       //
///////////////////////////

TEST (BankClientUpdateCountTest, GetAndIncrementTest)
  {
  const char* filename = "count_test.dat";
  system ("rm -f count_test.dat");

  EXPECT_EQ (0, GetExpectedUpdateCount (filename));
  IncrementExpectedUpdateCount (filename);
  EXPECT_EQ (1, GetExpectedUpdateCount (filename));
  IncrementExpectedUpdateCount (filename);
  EXPECT_EQ (2, GetExpectedUpdateCount (filename));
  IncrementExpectedUpdateCount (filename);
  EXPECT_EQ (3, GetExpectedUpdateCount (filename));
  }

//class ClientRpcCallTest : public ::testing::Test
//  {
//  protected:
//    virtual void SetUp () 
//      {
//      const char* svc = "s10v6c";
//	    clnt = clnt_create (svc, SyncAgent, SyncAgentVer, "udp");
//	    if (clnt == NULL) 
//        {
//	      clnt_pcreateerror (svc);
//	      throw runtime_error ("ClientRpcCallTest unable to create a client handle for a remote server!");
//        }
//      }
//
//    virtual void TearDown () 
//      {
//      clnt_destroy (clnt);
//      }
//
//    CLIENT* clnt;
//  };
//
//TEST_F (ClientRpcCallTest, DoDepositTest)
//  {
//  EXPECT_TRUE (DoDeposit ("wilson", clnt));
//  }
//
//TEST_F (ClientRpcCallTest, DoWithdrawalTest)
//  {
//  EXPECT_TRUE (DoWithdrawal ("wilson", clnt));
//  }
//
//TEST_F (ClientRpcCallTest, DoTransferTest)
//  {
//  EXPECT_TRUE (DoTransfer ("wilson", clnt));
//  }
//
//TEST_F (ClientRpcCallTest, DoReadBalanceTest)
//  {
//  EXPECT_TRUE (DoReadBalance ("wilson", clnt));
//  }


/////////////////////////
//                     //
// Program start point //
//                     //
/////////////////////////

int main(int argc, char **argv) 
  {
  // print help
  if (argc == 1 ||
      FindCommandlineKey (argc, argv, "--help")  || 
      FindCommandlineKey (argc, argv, "-h"))
    {
    cout << "Usage: " << argv[0] << " -p=HOSTNAME -b=HOSTNAME [Bank Server Options] [Test Selection]" << endl;
    cout                                                                                              << endl;
    cout << "Bank Server Options:"                                                                    << endl;
    cout                                                                                              << endl;
    cout << "   -p --primary=HOSTNAME [required] hostname of the primary Bank server"                 << endl;
    cout << "   -b --backup=HOSTNAME  [required] hostname of the backup Bank server"                  << endl;
    cout << "   -h --help             show this message"                                              << endl;
    cout << "      --gtest            run the self-test module"                                       << endl;
    cout                                                                                              << endl;

    argc = 2;
    argv[1] = "--help";
    ::testing::InitGoogleTest (&argc, argv);
    int ret = RUN_ALL_TESTS();

    cout                                                                                              << endl;
    cout << "Author: THONG, Wang Kit Wilson (50419704)"                                               << endl;
    cout << "Remark: This program was written in April 2011, for the course CS6223"                   << endl;
    cout << "offered by CityU of HK. The source code can be downloaded from"                          << endl;
    cout << "http://code.google.com/p/cs6223-2011-bank-system/source/list"                            << endl;
    cout                                                                                              << endl;

    exit (ret);
    }

  if (FindCommandlineKey (argc, argv, "--gtest") ||
      ContainCommandlineKey (argc, argv, "--gtest_"))
    {
    ::testing::InitGoogleTest (&argc, argv);
    exit (RUN_ALL_TESTS());
    }

  string primarySvc;
  if (ParseCommandlineArg (argc, argv, "--primary=", primarySvc) || 
      ParseCommandlineArg (argc, argv, "-p=", primarySvc))
    {}
  else
    {
    DEBUG_ERR ("Missing --primary argument!");
    exit (0);
    }

  string backupSvc;
  if (ParseCommandlineArg (argc, argv, "--backup=", backupSvc) || 
      ParseCommandlineArg (argc, argv, "-b=", backupSvc))
    {}
  else
    {
    DEBUG_ERR ("Missing --backup argument!");
    exit (0);
    }

  BankClient client (primarySvc, backupSvc);
  client.Run ();

  }
